﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Markup;
using SqlMiM.Common.Controls;
using System.Windows.Threading;

namespace SqlMiM.Common.UserControls.PerfGraph
{
    public partial class HistoryGraph : UserControl, INotifyPropertyChanged, IComponentConnector
    {
        // Fields

        // Maxe de l'axe Y pour les valeurs distinctes  
        private double _currentMaxValue = double.NaN;

        // Dictionnaire de HisttoryData
        private List<HistoryData> _dataList = new List<HistoryData>();

        // Dictionnaire des HistoryData
        private Dictionary<object, HistoryData> _dataTable = new Dictionary<object, HistoryData>();

        // Dictionnaire des Figure
        private Dictionary<object, PathFigure> _dataPathFigure = new Dictionary<object, PathFigure>();

        // Représente une collection de dessins qui peuvent être traités comme un seul dessin
        private DrawingGroup _drawingGroup;

        // Group englobant l'HistoryGraph
        private HistoryGraphGroup _group;

        // Mise à jour des données en cours ou non
        private bool _isUpdating;

        private DateTime _lastScrollTime = DateTime.UtcNow;

        // Date de dernière mise à jour
        private DateTime _lastUpdate = DateTime.UtcNow;

        private bool _needsRefresh;

        // Nombre d'HistoryData traités
        private int _numberOfHistoryDataPopulated;

        // Index du premier HistoryData traité (il se peut que le premier n'est pas de données par ex)
        private int _firstHistoryDataPopulated = -1;

        // Réprésente la gémotrie complexe à représenter
        private PathGeometry _pathGeometry;

        // Comparer
        private PointXValueComparer _pointComparer = new PointXValueComparer();

        // Callback pour une demande de refresh async
        private DispatcherOperationCallback _refreshCallback;


        #region Dependency Properties

        /// <summary>
        /// Première intervalle visible (permet de voir le graph commencer à 30 sec par ex.
        /// </summary>
        public static readonly DependencyProperty FirstTimeVisibleProperty = DependencyProperty.Register("FirstTimeVisible",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan(), new PropertyChangedCallback(HistoryGraph.OnPointsVisibleChanged)));

        private static void OnPointsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HistoryGraph graph = (HistoryGraph)d;
            graph.InvalidateMaxValues();
            graph.UpdateYMaxValue();
            graph.NotifyPropertyChanged(e.Property.Name);
        }

        public static readonly DependencyProperty FixedMaxValueProperty = DependencyProperty.Register("FixedMaxValue",
            typeof(double), typeof(HistoryGraph), new UIPropertyMetadata(0.01, new PropertyChangedCallback(HistoryGraph.OnFixedMaxChanged),
                new CoerceValueCallback(HistoryGraph.CoerceFixedMax)));

        private static void OnFixedMaxChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            HistoryGraph graph = (HistoryGraph)d;

            if (graph.GraphType == HistoryGraphType.FixedMax)
                graph.UpdateYMaxValue();
        }

        /// <summary>
        /// Valide la valeur de la DP FixedMaxValue
        /// </summary>
        private static object CoerceFixedMax(DependencyObject d, object baseValue)
        {
            double fixedMaxValue = (double)baseValue;
            if ((!double.IsNaN(fixedMaxValue) && !double.IsInfinity(fixedMaxValue)) && (fixedMaxValue > 0.0))
            {
                return fixedMaxValue;
            }

            return 0.01;
        }

        /// <summary>
        /// Durée du Graph
        /// </summary>
        public static readonly DependencyProperty GraphDurationProperty = DependencyProperty.Register("GraphDuration",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan()));

        /// <summary>
        /// Type du graphique
        ///     Relative,
        ///     FixedMax,
        ///     GraphMax
        /// Attention : La valeur Y MAX est racalculé sur le type Relative et GraphMax
        /// alors qu'elle prend FixedMaxValue si le type est FixedMax
        /// </summary>
        public static readonly DependencyProperty GraphTypeProperty = DependencyProperty.Register("GraphType",
            typeof(HistoryGraphType), typeof(HistoryGraph), new UIPropertyMetadata(HistoryGraphType.Relative, new PropertyChangedCallback(HistoryGraph.OnGraphTypeChanged)), new ValidateValueCallback(HistoryGraph.ValidateGraphType));

        /// <summary>
        /// Rafraichit les données si on change de graphe ou de type de graphe (valeurs globales ou distinctes)
        /// </summary>
        private static void OnGraphTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((HistoryGraph)d).RefreshData();
        }

        /// <summary>
        /// Axe X : Interval Temps (Secondes) entre chaque barre majeur
        /// </summary>
        public static readonly DependencyProperty MajorTimeIntervalProperty = DependencyProperty.Register("MajorTimeInterval",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan()));

        /// <summary>
        /// Axe X : Interval Temps (Secondes) entre chaque barre mineur
        /// </summary>
        public static readonly DependencyProperty MinorTimeIntervalProperty = DependencyProperty.Register("MinorTimeInterval",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan()));

        /// <summary>
        /// Axe Y : Interval Entier entre chaque barre majeur
        /// </summary>
        public static readonly DependencyProperty MajorValueIntervalProperty = DependencyProperty.Register("MajorValueInterval",
            typeof(int), typeof(HistoryGraph), new UIPropertyMetadata(0));

        /// <summary>
        /// Axe X : Interval Entier entre chaque barre mineur
        /// </summary>
        public static readonly DependencyProperty MinorValueIntervalProperty = DependencyProperty.Register("MinorValueInterval",
            typeof(int), typeof(HistoryGraph), new UIPropertyMetadata(0));

        public static readonly DependencyProperty ScrollBrushProperty = DependencyProperty.Register("ScrollBrush",
            typeof(Brush), typeof(HistoryGraph), new UIPropertyMetadata(Brushes.Gray));

        public static readonly DependencyProperty ScrollIntervalProperty = DependencyProperty.Register("ScrollInterval",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan()));

        public static readonly DependencyProperty VerticalAxisConverterProperty = DependencyProperty.Register("VerticalAxisConverter",
            typeof(ValueToLabelHandler), typeof(HistoryGraph), new UIPropertyMetadata(null));

        public static readonly DependencyProperty VisibleIntervalProperty = DependencyProperty.Register("VisibleInterval",
            typeof(TimeSpan), typeof(HistoryGraph), new UIPropertyMetadata(new TimeSpan(), new PropertyChangedCallback(HistoryGraph.OnPointsVisibleChanged)));


        private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if ((bool)e.NewValue)
                this.RefreshData();
        }

        #endregion


        #region Evènements (PropertyChanged)

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(string info)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        #endregion


        #region Propriétés

        private double CurrentMaxValue
        {
            get
            {
                if (double.IsNaN(this._currentMaxValue))
                {
                    this.ComputeCurrentMaxValue();
                }
                return this._currentMaxValue;
            }
        }


        public TimeSpan FirstTimeVisible
        {
            get
            {
                return (TimeSpan)base.GetValue(FirstTimeVisibleProperty);
            }
            set
            {
                base.SetValue(FirstTimeVisibleProperty, value);
            }
        }

        public double FixedMaxValue
        {
            get
            {
                return (double)base.GetValue(FixedMaxValueProperty);
            }
            set
            {
                base.SetValue(FixedMaxValueProperty, value);
            }
        }

        public TimeSpan GraphDuration
        {
            get
            {
                return (TimeSpan)base.GetValue(GraphDurationProperty);
            }
            set
            {
                base.SetValue(GraphDurationProperty, value);
            }
        }

        public HistoryGraphType GraphType
        {
            get
            {
                return (HistoryGraphType)base.GetValue(GraphTypeProperty);
            }
            set
            {
                base.SetValue(GraphTypeProperty, value);
            }
        }

        public HistoryGraphGroup Group
        {
            get
            {
                return this._group;
            }
            set
            {
                if (value == null)
                {
                    value = new HistoryGraphGroup(this._group.UpdateInterval);
                }
                value.SwitchGroup(this);
                this._group = value;
            }
        }

        public bool IsRunning
        {
            get;
            set;
        }

        public TimeSpan MajorTimeInterval
        {
            get
            {
                return (TimeSpan)base.GetValue(MajorTimeIntervalProperty);
            }
            set
            {
                base.SetValue(MajorTimeIntervalProperty, value);
            }
        }

        public int MajorValueInterval
        {
            get
            {
                return (int)base.GetValue(MajorValueIntervalProperty);
            }
            set
            {
                base.SetValue(MajorValueIntervalProperty, value);
            }
        }

        public TimeSpan MinorTimeInterval
        {
            get
            {
                return (TimeSpan)base.GetValue(MinorTimeIntervalProperty);
            }
            set
            {
                base.SetValue(MinorTimeIntervalProperty, value);
            }
        }

        public int MinorValueInterval
        {
            get
            {
                return (int)base.GetValue(MinorValueIntervalProperty);
            }
            set
            {
                base.SetValue(MinorValueIntervalProperty, value);
            }
        }


        public Brush ScrollBrush
        {
            get
            {
                return (Brush)base.GetValue(ScrollBrushProperty);
            }
            set
            {
                base.SetValue(ScrollBrushProperty, value);
            }
        }

        public TimeSpan ScrollInterval
        {
            get
            {
                return (TimeSpan)base.GetValue(ScrollIntervalProperty);
            }
            set
            {
                base.SetValue(ScrollIntervalProperty, value);
            }
        }

        public TimeSpan UpdateInterval
        {
            get
            {
                return this._group.UpdateInterval;
            }
            set
            {
                this._group.UpdateInterval = value;
            }
        }

        public ValueToLabelHandler VerticalAxisConverter
        {
            get;
            set;

        }

        public TimeSpan VisibleInterval
        {
            get
            {
                return (TimeSpan)base.GetValue(VisibleIntervalProperty);
            }
            set
            {
                base.SetValue(VisibleIntervalProperty, value);
            }
        }

        public double YMaxValue
        {
            get;
            set;

        }

        #endregion

        public HistoryGraph()
        {
            InitializeComponent();

            // Group englobant l'HistoryGraph
            this.Group = new HistoryGraphGroup();

            // Callback de Refresh
            this._refreshCallback = new DispatcherOperationCallback(this.DoRefresh);

            this.InitializeComponent();

            this.InitializeRenderData();

            this._xAxisTimeline.HashLabelConverter = new ValueToLabelHandler(this.ConvertTimeToString);

            base.IsVisibleChanged += new DependencyPropertyChangedEventHandler(this.OnIsVisibleChanged);
        }


        private void InitializeRenderData()
        {

            // Représente une collection de dessins qui peuvent être traités comme un seul dessin
            this._drawingGroup = new DrawingGroup();

            // Nouvelle géométrie complexe
            this._pathGeometry = new PathGeometry();

            this._renderBrush.ImageSource = new DrawingImage(this._drawingGroup);

        }

        /// <summary>
        /// Ajoute un HistoryData à l'HistoryGraph
        /// </summary>
        public void AddHistoryData(HistoryData historyData)
        {
            if (historyData == null)
                throw new ArgumentNullException("historyData");

            this._dataTable.Add(historyData.Name, historyData);
            this._dataList.Add(historyData);

            // Closed ou pas (Shape ou Path)
            bool isClosedAndFilled = historyData.Type == HistoryDataType.Shape;

            // Rajout de ma figure représentant mon HistoryData
            PathFigure figure = new PathFigure(new Point(), new LineSegment[0], isClosedAndFilled);
            figure.IsFilled = isClosedAndFilled;

            // Ajout dans ma collection
            this._dataPathFigure.Add(historyData.Name, figure);

            // Ajout de cette figure à ma géométrie complexe
            this._pathGeometry.Figures.Add(figure);

        }

        #region RAZ des données (avec suppression des HistoryData ou nn

        /// <summary>
        /// Effacer les données
        /// </summary>
        public void ClearData()
        {
            this.ClearData(false);
        }

        /// <summary>
        /// Efface les données et supprime ou non les HistoryData contenus
        /// </summary>
        public void ClearData(bool removeHistoryData)
        {
            // Raz des HistoryData
            for (int i = 0; i < this._dataList.Count; i++)
                this._dataList[i].ClearData();

            // Supprime les HistoryData si demandé
            if (removeHistoryData)
            {
                this._dataTable.Clear();
                this._dataList.Clear();
                this._dataPathFigure.Clear();
            }

            this._numberOfHistoryDataPopulated = 0;
            this._firstHistoryDataPopulated = -1;

            // Refresh des données
            this.RefreshData();
        }

        #endregion


        #region Recalcul des valeurs Max de Y

        /// <summary>
        /// Rassemble l'ensemble des valeurs distinctes. Recalcul le max des Y
        /// </summary>
        private void ComputeCurrentMaxValue()
        {
            this._currentMaxValue = 0.0;

            if (this._numberOfHistoryDataPopulated > 0)
            {
                foreach (HistoryData data in this._dataList)
                {

                    if (data.HasData)
                    {
                        Point point = new Point(this.FirstTimeVisible.TotalSeconds, 0.0);
                        Point point2 = new Point(point.X + this.VisibleInterval.TotalSeconds, 0.0);
                        
                        Point[] currentPoints = data.ComputePoints(this._lastUpdate);
                        
                        int num = Array.BinarySearch<Point>(currentPoints, point, this._pointComparer);
                        int num2 = Array.BinarySearch<Point>(currentPoints, point2, this._pointComparer);
                        if (num < 0)
                        {
                            num = ~num - 1;
                            if (num < 0)
                            {
                                num = 0;
                            }
                        }
                        if (num2 < 0)
                        {
                            num2 = ~num2;
                            if (num2 >= currentPoints.Length)
                            {
                                num2 = currentPoints.Length - 1;
                            }
                        }
                        for (int i = num; i <= num2; i++)
                        {
                            double y = currentPoints[i].Y;
                            if (y > this._currentMaxValue)
                            {
                                this._currentMaxValue = y;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        ///  Met à jour la valeur Y max
        ///  Se cantonne à récupéer soit le CurrentMaxValue soit le FixedMaxValue soit le AggregateMaxValue
        /// </summary>
        private void UpdateYMaxValue()
        {
            double height = 0.0;
            double yMaxValue = this.YMaxValue;
            this.YMaxValue = 0.0;

            switch (this.GraphType)
            {
                case HistoryGraphType.Relative:
                    this.YMaxValue = this.CurrentMaxValue;
                    height = this.YMaxValue;
                    break;

                case HistoryGraphType.FixedMax:
                    this.YMaxValue = this.FixedMaxValue;
                    height = this.YMaxValue;
                    break;

                case HistoryGraphType.GraphMax:
                    this.YMaxValue = this.CurrentMaxValue * 1.125;
                    height = this.YMaxValue;
                    break;

                default:
                    throw new NotImplementedException("Unhandled HistoryGraphType: " + this.GraphType);
            }
            this._renderBrush.Viewbox = new Rect(this.FirstTimeVisible.TotalSeconds, 0.0, this.VisibleInterval.TotalSeconds, height);

            if (this.YMaxValue != yMaxValue)
                this.NotifyPropertyChanged("YMaxValue");
        }

        #endregion


        #region Demande de refresh

        /// <summary>
        /// Invoque le refresh
        /// </summary>
        private void RefreshData()
        {
            if (!this._needsRefresh)
            {
                this._needsRefresh = true;
                base.Dispatcher.BeginInvoke(DispatcherPriority.Normal, this._refreshCallback, null);
            }
        }

        /// <summary>
        /// Refresh Asynchrone. Rafraichit les données distinctes ou globales
        /// </summary>
        private object DoRefresh(object unused)
        {
            this._needsRefresh = false;

            if (base.IsVisible)
            {
                this.UpdateYMaxValue();

                using (DrawingContext context = this._drawingGroup.Open())
                {
                    if (this._numberOfHistoryDataPopulated > 0)
                        this.RenderCurrentPoints(context);
                }
            }
            return null;
        }

        #endregion



        /// <summary>
        /// Invalide les 2 données de Max Y (aggrégés et distinctes)
        /// </summary>
        private void InvalidateMaxValues()
        {
            this._currentMaxValue = double.NaN;
        }

        /// <summary>
        /// Supprime les datas
        /// </summary>
        /// <param name="historyData"></param>
        public void RemoveHistoryData(HistoryData historyData)
        {
            if (historyData == null)
                throw new ArgumentNullException("historyData");

            if (historyData == this.GetHistoryData(historyData.Name))
            {
                this._dataTable.Remove(historyData.Name);
                this._dataPathFigure.Remove(historyData.Name);
                this._dataList.Remove(historyData);
            }
        }

        /// <summary>
        /// Rend les points distincts.
        /// Récupère les points recalculés
        /// </summary>
        private void RenderCurrentPoints(DrawingContext dc)
        {
            if (this._dataList == null)
                return;

            foreach (HistoryData data in this._dataList)
            {
                if (data.HasData)
                {
                    bool isClosedAndFilled = data.Type == HistoryDataType.Shape;

                    // Récupère les points distincts
                    Point[] currentDataPoints = data.ComputePoints(this._lastUpdate);

                    var pathFigure = this._dataPathFigure[data.Name];

                    // Clear des segments
                    pathFigure.Segments.Clear();

                    for (int i = 0; i < currentDataPoints.Length; i++)
                    {
                        Point point = currentDataPoints[i];
                        pathFigure.Segments.Add(new LineSegment(point, !isClosedAndFilled));
                    }

                    // dessin de la géométrie
                    if (isClosedAndFilled)
                    {
                        // Calcul du dernier X de gauche, pour fermer le Shape : 
                        // Minimum entre la fin du Graph ou le dernier élément du tableau
                        double x = Math.Min(this.GraphDuration.TotalSeconds, currentDataPoints[currentDataPoints.Length - 1].X + this.UpdateInterval.TotalSeconds);
                        // Ajout du dernier point
                        pathFigure.Segments.Add(new LineSegment(new Point(x, 0.0), true));

                        dc.DrawGeometry(data.Fill, null, (PathGeometry)this._pathGeometry.GetCurrentValueAsFrozen());
                    }
                    else
                    {
                        dc.DrawGeometry(null, data.Stroke, (PathGeometry)this._pathGeometry.GetCurrentValueAsFrozen());
                    }
                }
            }
        }

        #region Start, Stop, Pause

        public void Start()
        {
            this._group.Start();
        }

        internal void StartRunning(DateTime startTime)
        {
            this._lastScrollTime = startTime;
            if (!this.IsRunning)
            {
                this.IsRunning = true;
                this.NotifyPropertyChanged("IsRunning");
            }
        }

        public void Stop()
        {
            this._group.Stop();
        }

        internal void StopRunning()
        {
            if (this.IsRunning)
            {
                this.IsRunning = false;
                this.NotifyPropertyChanged("IsRunning");
            }
        }

        public void Pause()
        {
            this._group.Pause();
        }


        #endregion

        public void UpdateData()
        {
            this.UpdateData(this._lastUpdate);
        }

        /// <summary>
        /// Met à jour les données à un instant T
        /// Vide le tableau interne. 
        /// Va demander un refresh des Datas, qui va le re remplir
        /// </summary>
        public void UpdateData(DateTime currentTime)
        {
            if (!this._isUpdating)
            {
                this._isUpdating = true;

                // dernière mise à jour
                this._lastUpdate = currentTime;

                bool hasHistoryDataPopulated = false;

                int historyDataCount = this._dataList.Count;

                // Raz du nb de traités et de l'index du premier
                if (this._numberOfHistoryDataPopulated > 0)
                {
                    hasHistoryDataPopulated = true;
                    this._numberOfHistoryDataPopulated = 0;
                    this._firstHistoryDataPopulated = -1;
                }

                for (int i = 0; i < historyDataCount; i++)
                {
                    HistoryData data = this._dataList[i];
                    data.UpdateData(currentTime);
                    if (data.HasData)
                    {
                        hasHistoryDataPopulated = true;
                        this._numberOfHistoryDataPopulated++;

                        if (this._firstHistoryDataPopulated == -1)
                            this._firstHistoryDataPopulated = i;
                    }
                }

                // Une fois tous les HistoryData traités
                // Raz du tableau interne
                // Raz des valeurs max
                // Demande de refresh des données (et donc re remplissage du tableau interne)
                if (hasHistoryDataPopulated)
                {
                    // Raz du Max Y
                    this.InvalidateMaxValues();

                    // mise à jour des données, et du tab interne
                    this.RefreshData();
                }

                // ?????
                if (this.ScrollInterval.Ticks > 0L)
                {
                    while ((currentTime - this._lastScrollTime) > this.ScrollInterval)
                    {
                        this._lastScrollTime += this.ScrollInterval;
                    }
                    TimeSpan span = this.ScrollInterval - (currentTime - this._lastScrollTime);
                    //this._graphScroll.FirstPointVisible = span.TotalSeconds;
                }

                // Fin de la mise à jour
                this._isUpdating = false;
            }
        }



        #region Divers

        private string ConvertTimeToString(double value)
        {
            string str = TimeSpan.FromSeconds(Math.Round(value)).ToString();
            if (str.StartsWith("00:"))
            {
                str = str.Substring(3);
            }
            return str;
        }

        public HistoryData GetHistoryData(string name)
        {
            HistoryData data;
            this._dataTable.TryGetValue(name, out data);
            return data;
        }

        private static bool ValidateGraphType(object value)
        {
            switch (((HistoryGraphType)value))
            {
                case HistoryGraphType.Relative:
                case HistoryGraphType.FixedMax:
                case HistoryGraphType.GraphMax:
                    return true;
            }
            return false;
        }

        #endregion


        // Nested Types
        private class PointXValueComparer : IComparer<Point>
        {
            // Methods
            public int Compare(Point p1, Point p2)
            {
                return p1.X.CompareTo(p2.X);
            }
        }
    }


}
